跳到主要内容

CLI Parameter Types

数据类型校验与转换

为 CLI parameter 添加类型提示后,Typer 会自动将参数转换成对应的数据类型

def main(name: str, age: int = 20, height_meters: float = 1.89, female: bool = True):
print(f"NAME is {name}, of type: {type(name)}")
print(f"--age is {age}, of type: {type(age)}")
print(f"--height-meters is {height_meters}, of type: {type(height_meters)}")
print(f"--female is {female}, of type: {type(female)}")
  • NAME 会被当做 str 来看待

  • --age 会被转换成 int--height-meters 会被转换成 float

  • female 是一个 bool CLI option,所以 --female 是 True,--no-female 则为 False

image-20240711173303274

Number

使用 max and min 来为 int 或 float 类型添加范围限制

def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
age: Annotated[int, typer.Option(min=18)] = 20,
score: Annotated[float, typer.Option(max=100)] = 0,
):
print(f"ID is {id}")
print(f"--age is {age}")
print(f"--score is {score}")
image-20240711174809422

使用上下限而不是报错

def main(
id: Annotated[int, typer.Argument(min=0, max=1000)],
rank: Annotated[int, typer.Option(max=10, clamp=True)] = 0,
score: Annotated[float, typer.Option(min=0, max=100, clamp=True)] = 0,
):
print(f"ID is {id}")
print(f"--rank is {rank}")
print(f"--score is {score}")

count 类型

You can make a CLI option work as a counter with the count parameter:

def main(verbose: Annotated[int, typer.Option("--verbose", "-v", count=True)] = 0):
print(f"Verbose level is {verbose}")

--verbose 就变成了一个计数类型的参数了

image-20240711181715327

Boolean 类型

bool 类型的 CLI Options,Typer 会默认使用形如 --something--no-something 来表示

当然,我们也是可以自定义这一行为的

禁用否定形式

def main(force: Annotated[bool, typer.Option("--force")] = False):

自定义否定形式

typer.Option 中传入一个字符串,用 / 隔开,分别表示肯定形式和否定形式

def main(accept: Annotated[Optional[bool], typer.Option("--accept/--reject")] = None):
image-20240712102055399

短名

let's say we want -f for --force and -F for --no-force:

def main(force: Annotated[bool, typer.Option("--force/--no-force", "-f/-F")] = False):
image-20240712102654880

只要否定形式

那就别写肯定形式

注意 / 前有空格!

def main(in_prod: Annotated[bool, typer.Option(" /--demo", " /-d")] = True):
image-20240712102738564

不过这样写的话,注释说明就很难看得懂了

UUID

你可以将 CLI Option 定义为 UUID

from uuid import UUID

import typer


def main(user_id: UUID):
print(f"USER_ID is {user_id}")
print(f"UUID version is: {user_id.version}")


if __name__ == "__main__":
typer.run(main)
image-20240712103206611

DateTime

from datetime import datetime

import typer


def main(birth: datetime):
print(f"Interesting day to be born: {birth}")
print(f"Birth hour: {birth.hour}")


if __name__ == "__main__":
typer.run(main)

Typer will accept any string from the following formats:

  • %Y-%m-%d
  • %Y-%m-%dT%H:%M:%S
  • %Y-%m-%d %H:%M:%S
image-20240712103550036

自定义日期字符串格式

def main(
launch_date: Annotated[
datetime,
typer.Argument(
formats=["%Y-%m-%d", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M:%S", "%m/%d/%Y"]
),
],
):
print(f"Launch will be at: {launch_date}")

Enum

from enum import Enum

import typer


class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"


def main(network: NeuralNetwork = NeuralNetwork.simple):
print(f"Training neural network of type: {network.value}")


if __name__ == "__main__":
typer.run(main)

Notice that the function parameter network will be an Enum, not a str.

To get the str value in your function's code use network.value.

image-20240712104345284

大小写敏感

默认情况下,Enum 类型的 CLI Parameter 是大小写敏感的,可以用 case_sensitive 来控制

class NeuralNetwork(str, Enum):
simple = "simple"
conv = "conv"
lstm = "lstm"


def main(
network: Annotated[
NeuralNetwork, typer.Option(case_sensitive=False)
] = NeuralNetwork.simple,
):
print(f"Training neural network of type: {network.value}")
image-20240712104556366

Enum 列表

A CLI parameter can also take a list of Enum values:

class Food(str, Enum):
food_1 = "Eggs"
food_2 = "Bacon"
food_3 = "Cheese"


def main(groceries: Annotated[List[Food], typer.Option()] = [Food.food_1, Food.food_3]):
print(f"Buying groceries: {', '.join([f.value for f in groceries])}")
image-20240712104824621

Path

You can declare a CLI parameter to be a standard Python pathlib.Path.

This is what you would do for directory paths, file paths, etc:

from pathlib import Path
from typing import Optional

import typer
from typing_extensions import Annotated


def main(config: Annotated[Optional[Path], typer.Option()] = None):
if config is None:
print("No config file")
raise typer.Abort()
if config.is_file():
text = config.read_text()
print(f"Config file contents: {text}")
elif config.is_dir():
print("Config is a directory, will use all its config files")
elif not config.exists():
print("The config doesn't exist")


if __name__ == "__main__":
typer.run(main)
image-20240712104946659

当然会有自动补全啦

校验选项

  • exists: if set to true, 文件或目录必须得存在. If this is false 而且文件确实不存在,那之后的校验都不会进行了
  • file_okay: 校验是否是文件
  • dir_okay: 校验是否是目录
  • writable: if true, 会检查是否可写
  • readable: if true, 会检查是否可读
  • resolve_path: if this is true, 文件会被转换为绝对路径(软连接也会哦)
    • 带有 ~ 的路径不会被展开
from pathlib import Path

import typer
from typing_extensions import Annotated


def main(
config: Annotated[
Path,
typer.Option(
exists=True,
file_okay=True,
dir_okay=False,
writable=False,
readable=True,
resolve_path=True,
),
],
):
text = config.read_text()
print(f"Config file contents: {text}")


if __name__ == "__main__":
typer.run(main)
image-20240712110206356

File

允许 CLI Parameters 以文件形式读 / 写文件

详见:https://typer.tiangolo.com/tutorial/parameter-types/file/

Custom Types

两种方式,使 Typer 支持你自定义的类:

  1. 定义 parser 函数来解析数据
  2. 定义 Click's custom types
import typer
from typing_extensions import Annotated


class CustomClass:
def __init__(self, value: str):
self.value = value

def __str__(self):
return f"<CustomClass: value={self.value}>"


def parse_custom_class(value: str):
return CustomClass(value * 2)


def main(
custom_arg: Annotated[CustomClass, typer.Argument(parser=parse_custom_class)],
custom_opt: Annotated[CustomClass, typer.Option(parser=parse_custom_class)] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")


if __name__ == "__main__":
typer.run(main)

import click
import typer
from typing_extensions import Annotated


class CustomClass:
def __init__(self, value: str):
self.value = value

def __repr__(self):
return f"<CustomClass: value={self.value}>"


class CustomClassParser(click.ParamType):
name = "CustomClass"

def convert(self, value, param, ctx):
return CustomClass(value * 3)


def main(
custom_arg: Annotated[CustomClass, typer.Argument(click_type=CustomClassParser())],
custom_opt: Annotated[
CustomClass, typer.Option(click_type=CustomClassParser())
] = "Foo",
):
print(f"custom_arg is {custom_arg}")
print(f"--custom-opt is {custom_opt}")


if __name__ == "__main__":
typer.run(main)